import pickle, random
from collections import defaultdict

from config import current as conf

class SimpleEventDispatcher(object):
    """Allows functions to be registered for events by name"""
    def __init__(self):
        super(SimpleEventDispatcher, self).__init__()
        self.handlers = defaultdict(set)

    def handle(self, event, fn):
        self.handlers[event].add(fn)

    def trigger(self, event, *args, **kwargs):
        for handler in self.handlers[event]:
            handler(*args, **kwargs)

    def __getstate__(self):
        """Ensure that the handlers do not get pickled"""
        odict = self.__dict__.copy()
        del odict['handlers']
        return odict

    def __setstate__(self, dict):
        """When unpickled, the handler set is empty"""
        self.__dict__.update(dict)
        self.handlers = defaultdict(set)

def load(filename):
    """Load a game state from a file.
       This will probably be use both for saved games as well as initial
       level loading."""
    f = open(filename, 'rb')
    state = None
    try:
        state = pickle.load(f)
    finally:
        f.close()    
    return state

class GameState(SimpleEventDispatcher):
    """Holds all game state -- anything that would need to be saved"""
    def __init__(self):
        super(GameState, self).__init__()
        self.new_game = True
        self.bg_filename = ""
        self.cities = set()
        self.hordes = set()
        self.spawn_pts = set()
        self.solid_pts = set()
        self.dimensions = (32,24)
        self.research = 0.0
        self.current_year = 2010
        self.clean_cities = [] # This is for selecting a favorite city.

    def advance(self):
        """Advance the game state by one turn"""
        # spawn zombies at infected cities
        for spawn_pt in [c.location for c in self.cities if c.status == City.INFECTED]:
            if random.random() < conf.horde_spawn_rate_cities:
                new = Horde(self, spawn_pt, favorite_city=random.choice(self.clean_cities))
                self.hordes.add(new)
                self.trigger("new_horde", new)
        # spawn zombies at other spawn pts
        for spawn_pt in self.spawn_pts:
            if random.random() < conf.horde_spawn_rate_other:
                new = Horde(self, spawn_pt, favorite_city=random.choice(self.clean_cities))
                self.hordes.add(new)
                self.trigger("new_horde", new)
        
        # cities attack hordes
        for city in self.cities:
            city.trigger("update")
            if city.status == City.INFECTED:
                continue
            cx, cy = city.location
            rad = 3*(city.defense.value()*city.population/conf.pop_radius_contribution)+1
            pow = city.defense.value()*0.5+city.population/conf.pop_power_contribution
            for horde in list(self.hordes):
                hx, hy = horde.location
                if abs(cx-hx) <= rad and abs(cy-hy) <= rad:
                    city.trigger("attack", horde.location)
                    horde.health -= random.random()*pow
                    if horde.health <= 0:
                        horde.trigger("death")
                        self.hordes.remove(horde)
            for ocity in self.cities:
                if ocity.status == City.INFECTED:
                    ix,iy = ocity.location
                    if abs(cx-ix) <= rad and abs(cy-iy) <= rad:
                        city.trigger("attack", ocity.location)
                        horde.health -= random.random()*0.2
                        ocity.wall -= pow*0.01
                        ocity.trigger("update")
                        if ocity.wall <= 0:
                            ocity.wall = 1.0
                            ocity.status = City.CLEAN
                            self.clean_cities.append(ocity)
                            ocity.maxpop=0
                            ocity.change_population(0.25 * city.population, self)
                            city.change_population(city.population * 0.75, self)
                            ocity.trigger("flip")
                            ocity.trigger("update")
                            
        

        # apply work done by each city
        self.current_year += 1
        for city in self.cities:
            if city.status == City.CLEAN:
                self.research += city.population * city.research.value()/conf.total_research_goal
                new_pop = city.population * conf.population_growth_rate * 0.5
                new_pop += city.population * city.idle_percentage * conf.population_growth_rate
                city.change_population(city.population + new_pop, self)
                city.wall = min(1.0, city.wall + city.scavenging.value()*0.1)
        self.research = min(self.research, 1.0)# move zombies
        
        # Move zombies. Also make them harm harmans.
        for horde in self.hordes:
            horde.move()
        
        for city in self.cities:
            
            city.trigger("update") #Yeah, we shouldn't need to do this anywhere but here.

        # check for win/loss
        if self.research == 1.0:
            self.trigger("win")
        if all(city.status == City.INFECTED for city in self.cities):
            self.trigger("loss")


    def save(self, filename):
        """Save this game state to a file"""
        f = open(filename, 'wb')
        try:
            pickle.dump(self, f)
        finally:
            f.close()

class Statistic(object):
    AMT = 0.08
    def __init__(self, city):
        super(Statistic, self).__init__()
        self.city = city
        self.val = 0.0
    def value(self): return self.val
    def incr(self):
        new_idle = max(self.city.idle_percentage - self.AMT, 0.0)
        self.val += (self.city.idle_percentage - new_idle)
        self.city.idle_percentage = new_idle
    def decr(self):
        new_val = max(self.val - self.AMT, 0.0)
        self.city.idle_percentage += (self.val - new_val)
        self.val = new_val

class City(SimpleEventDispatcher):
    """Game state of a single city"""
    INFECTED, CLEAN = 0, 1
    def __init__(self, state, name="My name", population=10000, location=(0,0), status = INFECTED):
        super(City, self).__init__()
        self.location = location
        self.name = name
        self.population = population
        self.status = status
        self.wall = 1.0
        self.idle_percentage = 1.0
        self.research = Statistic(self)
        self.scavenging = Statistic(self)
        self.defense = Statistic(self)
        self.maxpop = population
        self.poprate = 1.0
        if self.status == City.CLEAN: state.clean_cities.append(self)
    
    def change_population(self, new_pop, state):
        self.maxpop=max(self.maxpop,new_pop)
        self.population=new_pop
        self.poprate=self.population/self.maxpop
        if self.status == City.CLEAN and self.population == 0:
            state.clean_cities.remove(self)
            self.status = City.INFECTED
            self.wall = 1.0
            self.trigger("flip")

class Horde(SimpleEventDispatcher):
    all_moves = [(-1,-1), (-1, 0), (-1,1), (0,-1), (0,1), (1,-1), (1, 0), (1,1)]
    def __init__(self, state, location = (0,0), health = 1.0, favorite_city = None):
        super(Horde, self).__init__()
        self.state = state
        self.location = location
        self.health = health
        self.favorite_city = favorite_city

    def possible_moves(self):
        x,y = self.location
        for dx, dy in self.all_moves:
            if 0 <= x+dx < self.state.dimensions[0] and \
               0 <= y+dy < self.state.dimensions[1] and \
               (x+dx, y+dy) not in self.state.solid_pts:
                yield (x+dx, y+dy)

    def move(self):
        old = self.location
        possible_moves = set(self.possible_moves())
        for c in self.state.cities:
            if c.status == City.CLEAN and c.location in possible_moves:
                return self.attack(c)
        if self.favorite_city!=None and self.favorite_city.status==City.INFECTED:
            self.favorite_city=None
        if self.favorite_city==None or random.random()>0.5:
            self.location = random.choice(list(self.possible_moves()))
        else:
            cx, cy = self.favorite_city.location
            self.location = min(list(self.possible_moves()), key=lambda a: abs(a[0]-cx+a[1]-cy))
        self.trigger("move", old, self.location)
    
    def attack(self, city):
        self.trigger("attack", city)
        if city.wall > 0:
            city.wall = max(0, city.wall - 0.1)
        else:
            old_pop = city.population
            city.change_population(city.population*0.9, self.state)
            city.change_population(max(city.population-10, 0), self.state)
        city.trigger("update")

class TestState(GameState):
    """A state with some simple content for testing"""
    def __init__(self):
        super(TestState, self).__init__()
        self.bg_filename = "us.png"
        self.cities.add(City(self, "Tulsa", 2000, (4, 12), City.CLEAN))
        self.cities.add(City(self, "Dallas", 20000, (4, 8)))
        self.cities.add(City(self, "Chicago", 20000, (16, 21)))
        self.cities.add(City(self, "Kansas City", 20000, (8, 16), City.CLEAN))
        self.spawn_pts.add((25,0))